﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-09-28 18:20
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

using NAudio;
using NAudio.Wave;

using BassDrop.Core.Common.Media;
using BassDrop.Core.Common.Utilities;
using BassDrop.Plugins.NAudioDecoder.Localization;
using BassDrop.Core.Common.Plugins;
using System.Threading;
using BassDrop.Core.Common.Media.Playlists;
using System.Threading.Tasks;

namespace BassDrop.Plugins.NAudioDecoder
{
	/// <summary>
	/// Wrapper around NAudio. Decodes mp3, wav and aiff.
	/// </summary>
	public class Decoder : MediaDecoderBase
	{
		/// <summary>
		/// Desired / preferred playback and position update latency in ms
		/// </summary>
		private const int PreferredLatency = 300;

		#region File format extensions

		/// <summary>
		/// Possible extensions for mp3 files
		/// </summary>
		public static readonly string[] ExtensionsForMp3 =
		{
			".mp3"
		};

		/// <summary>
		/// Possible extensions for wav files
		/// </summary>
		public static readonly string[] ExtensionsForWav =
		{
			".wav",
			".wave"
		};

		/// <summary>
		/// Possible extensions for aiff files
		/// </summary>
		public static readonly string[] ExtensionsForAiff =
		{
			".aiff",
			".aif",
			".aifc"
		};

		#endregion File format extensions

		#region Fields and properties

		/// <summary>
		/// Can be used to lock down everything in the decoder object
		/// </summary>
		private object m_decoderGlobalLock = new object();

		/// <summary>
		/// The media player device
		/// </summary>
		private IWavePlayer m_waveOutDevice;

		/// <summary>
		/// The output metering stream for the device
		/// </summary>
		private WaveStream m_mainOutputStream;
		/// <summary>
		/// The actual file stream for the device
		/// </summary>
		private WaveChannel32 m_volumeStream;

		/// <summary>
		/// The player is ready to play the current track
		/// </summary>
		private bool m_playerReady = false;

		#endregion Fields and properties

		/// <summary>
		/// Constructs a new Windows Media Element Decoder
		/// </summary>
		/// <param name="pluginDescriptor">The descriptor object of the plugin owning this object</param>
		/// <param name="instanceName">the name of this plugin instance</param>
		internal Decoder( BassDropPlugin pluginDescriptor, string instanceName )
			: base( pluginDescriptor, instanceName, 100 )
		{
			Options = new OptionsSet( pluginDescriptor, this );

			SetPlayer();
		}

		/// <summary>
		/// Mostly for debug purposes
		/// </summary>
		~Decoder()
		{
			System.Diagnostics.Debug.Assert( false, "BassDrop.Plugins.NAudioDecoder was not disposed!" );
			Dispose();
		}

		#region MediaDecoderBase implementation

		/// <summary>
		/// Starts playing the selected track from the selected position
		/// </summary>
		protected override void StartPlayback()
		{
			lock ( m_decoderGlobalLock )
			{
				if ( m_playerReady )
				{
					m_waveOutDevice.Play();
				}
				// TODO: else exception?
			}
		}

		/// <summary>
		/// Pauses playing the track
		/// </summary>
		protected override void PausePlayback()
		{
			lock ( m_decoderGlobalLock )
			{
				if ( m_playerReady )
				{
					m_waveOutDevice.Pause();
				}
				// TODO: else exception?
			}
		}

		/// <summary>
		/// Stops playing the track and resets Position to null
		/// </summary>
		protected override void StopPlayback()
		{
			lock ( m_decoderGlobalLock )
			{
				if ( m_playerReady )
				{
					m_waveOutDevice.Stop();
				}
				// TODO: else exception?
			}
		}

		/// <summary>
		/// Parses metadata from the file for the passed Track and returns it
		/// in a MediaTrack.Metadata object
		/// </summary>
		/// <param name="track">the Track to parse data from</param>
		/// <returns></returns>
		public override async Task<MediaTrack.Metadata> GetMetadataForTrack( MediaTrack track )
		{
			using ( Decoder decoder = (Decoder) await PluginDescriptor.GetTemporaryInstance() ) // HACK: ez így nem lassú?
			{
				decoder.Track = new PlaylistItemMediaTrack( track );
				return decoder.GetMetadataForCurrentTrack();
			}
		}

		/// <summary>
		/// Parses metadata from the current track and returns it
		/// in a MediaTrack.Metadata object
		/// </summary>
		/// <returns></returns>
		private MediaTrack.Metadata GetMetadataForCurrentTrack()
		{
			lock ( m_decoderGlobalLock )
			{
				if ( m_playerReady )
				{
					MediaTrack.Metadata data = new MediaTrack.Metadata();

					data.Length = TimeUtils.TimeSpanToMillis( m_mainOutputStream.TotalTime );

					// TODO: read ID3

					return data;
				}
				else
				{
					return null; // TODO: vagy exception?
				}
			}
		}

		/// <summary>
		/// Lock preventing multiple update threads to start RefreshPosition
		/// </summary>
		private object m_RefreshPosition_lock = new object();
		/// <summary>
		/// Called periodically to refresh the Position property from the player
		/// </summary>
		/// <param name="state">always null</param>
		protected override void RefreshPosition( object state )
		{
			if ( Monitor.TryEnter( m_RefreshPosition_lock ) ) // no point waiting
			{
				try
				{
					if ( m_playerReady )
					{
						// Some wave streams don't tell the device that they are over,
						// so a playback stopped event doesn't get fired.
						// To work around this, check if the playback should have ended by now
						if ( m_mainOutputStream.Position >= m_mainOutputStream.Length )
						{
							UpdatePosition( TimeUtils.TimeSpanToMillis( m_mainOutputStream.TotalTime ) );
							// fake a PlaybackStopped event
							m_waveOutDevice_PlaybackStopped( m_waveOutDevice, new StoppedEventArgs( null ) );
						}
						else if ( Position != null )
						{
							UpdatePosition( TimeUtils.TimeSpanToMillis( m_mainOutputStream.CurrentTime ) );
						}
					}
				}
				finally
				{
					Monitor.Exit( m_RefreshPosition_lock );
				}
			}
		}

		/// <summary>
		/// Called when the position is changed from the outside
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected override void MediaDecoderBase_PositionChanged( object sender, PropertyChangedEventArgs e )
		{
			if ( m_playerReady )
			{
				m_mainOutputStream.CurrentTime = TimeUtils.MillisToTimeSpan( (long) (Position ?? 0) );
			}
		}

		/// <summary>
		/// Called when the track is replaced with a new one
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected override void MediaDecoderBase_TrackChanged( object sender, PropertyChangedEventArgs e )
		{
			lock ( m_decoderGlobalLock )
			{
				SetPlayer();
			}
		}

		#endregion MediaDecoderBase implementation

		#region IDisposable Members

		/// <summary>
		/// Stops playback and disposes of the current player
		/// </summary>
		public override void Dispose()
		{
			lock ( m_decoderGlobalLock )
			{
				Stop();
				UnsetPlayer();
				GC.SuppressFinalize( this );
			}
		}

		#endregion IDisposable Members

		#region Player opening and closing

		/// <summary>
		/// Sets up the player to play the currently selected track
		/// </summary>
		private void SetPlayer()
		{
			lock ( m_decoderGlobalLock )
			{
				UnsetPlayer();

				if ( Track != null )
				{
					string format = Track.Track.FileExtension;

					if ( ExtensionsForAiff.Contains( format ) )
					{
						WaveStream aiffReader = new AiffFileReader( Track.Track.FilePath.LocalPath );
						m_volumeStream = new WaveChannel32( aiffReader );
						m_mainOutputStream = m_volumeStream;
					}
					else if ( ExtensionsForMp3.Contains( format ) )
					{
						WaveStream mp3Reader = new Mp3FileReader( Track.Track.FilePath.LocalPath );
						m_volumeStream = new WaveChannel32( mp3Reader );
						m_mainOutputStream = m_volumeStream;
					}
					else if ( ExtensionsForWav.Contains( format ) )
					{
						WaveStream wavReader = new WaveFileReader( Track.Track.FilePath.LocalPath );
						m_volumeStream = new WaveChannel32( wavReader );
						m_mainOutputStream = m_volumeStream;
					}
					else
					{
						throw new InvalidOperationException( PluginStrings.ExceptionInvalidFileFormat );
					}

					m_waveOutDevice = new WaveOutEvent();
					((WaveOutEvent) m_waveOutDevice).DesiredLatency = PreferredLatency;
					m_waveOutDevice.Init( m_mainOutputStream );

					m_playerReady = true;
				}
			}
		}

		/// <summary>
		/// Closes the current stream
		/// </summary>
		private void UnsetPlayer()
		{
			lock ( m_decoderGlobalLock )
			{
				m_playerReady = false;

				if ( m_waveOutDevice != null )
				{
					Stop();
					m_waveOutDevice.PlaybackStopped -= m_waveOutDevice_PlaybackStopped;
				}

				if ( m_mainOutputStream != null )
				{
					try
					{
						m_volumeStream.Dispose();
					}
					catch ( Exception ) // HACK: error suppression
					{
						// probably not important and can't really do anything about it anyway...
					}
					m_volumeStream = null;

					try
					{
						m_mainOutputStream.Dispose();
					}
					catch ( Exception ) // HACK: error suppression
					{
						// probably not important and can't really do anything about it anyway...
					}
					m_mainOutputStream = null;
				}

				if ( m_waveOutDevice != null )
				{
					try
					{
						m_waveOutDevice.Dispose();
					}
					catch ( Exception ) // HACK: error suppression
					{
						// probably not important and can't really do anything about it anyway...
					}
					m_waveOutDevice = null;
				}
			}
		}

		#endregion Player opening and closing

		#region Player events

		/// <summary>
		/// Sets the playback state of the track to finished
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		void m_waveOutDevice_PlaybackStopped( object sender, StoppedEventArgs e )
		{
			lock ( m_decoderGlobalLock )
			{
				if ( Track.CurrentPlayState == PlaylistItemBase.PlayState.Playing
					|| Track.CurrentPlayState == PlaylistItemBase.PlayState.Seeking )
				{
					Track.CurrentPlayState = PlaylistItemBase.PlayState.Finished;
				}
			}
		}

		#endregion Player events
	}
}
